﻿using System;
using System.Configuration;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;

namespace Nardax.ServiceModel
{
    /// <summary>
    /// Custom channel factory that allows a configuration file to be read for a specific location
    /// </summary>
    public sealed class CustomChannelFactory<T> : ChannelFactory<T>
    {
        private string _configurationPath;

        private string _endpointConfigurationName;
        public CustomChannelFactory(string configurationPath)
            : base(typeof(T))
        {

            _configurationPath = configurationPath;
            base.InitializeEndpoint((string)null, null);
        }

        public CustomChannelFactory(Binding binding, string configurationPath)
            : this(binding, (EndpointAddress)null, configurationPath)
        {
        }

        public CustomChannelFactory(ServiceEndpoint serviceEndpoint, string configurationPath)
            : base(typeof(T))
        {

            _configurationPath = configurationPath;
            base.InitializeEndpoint(serviceEndpoint);
        }

        public CustomChannelFactory(string endpointConfigurationName, string configurationPath)
            : this(endpointConfigurationName, null, configurationPath)
        {
        }

        public CustomChannelFactory(Binding binding, EndpointAddress endpointAddress, string configurationPath)
            : base(typeof(T))
        {

            _configurationPath = configurationPath;
            base.InitializeEndpoint(binding, endpointAddress);
        }

        public CustomChannelFactory(Binding binding, string remoteAddress, string configurationPath)
            : this(binding, new EndpointAddress(remoteAddress), configurationPath)
        {
        }

        public CustomChannelFactory(string endpointConfigurationName, EndpointAddress endpointAddress, string configurationPath)
            : base(typeof(T))
        {

            _configurationPath = configurationPath;
            _endpointConfigurationName = endpointConfigurationName;
            base.InitializeEndpoint(endpointConfigurationName, endpointAddress);
        }

        /// <summary>
        /// Loads the serviceEndpoint description from the specified configuration file
        /// </summary>
        protected override ServiceEndpoint CreateDescription()
        {
            ServiceEndpoint serviceEndpoint = base.CreateDescription();

            ExeConfigurationFileMap map = new ExeConfigurationFileMap();
            map.ExeConfigFilename = _configurationPath;

            Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);
            ServiceModelSectionGroup serviceModelGroup = ServiceModelSectionGroup.GetSectionGroup(config);

            ChannelEndpointElement selectedEndpoint = null;

            if (IsWildcardMatchNullOrEmpty(_endpointConfigurationName))
            {
                foreach (ChannelEndpointElement endpoint in serviceModelGroup.Client.Endpoints)
                {
                    if (endpoint.Contract == serviceEndpoint.Contract.ConfigurationName)
                    {
                        selectedEndpoint = endpoint;
                        break; // TODO: might not be correct. Was : Exit For
                    }
                }
            }
            else
            {
                foreach (ChannelEndpointElement endpoint in serviceModelGroup.Client.Endpoints)
                {
                    if (endpoint.Contract == serviceEndpoint.Contract.ConfigurationName && _endpointConfigurationName == endpoint.Name)
                    {
                        selectedEndpoint = endpoint;
                        break; // TODO: might not be correct. Was : Exit For
                    }
                }
            }

            if (selectedEndpoint != null)
            {
                if (serviceEndpoint.Binding == null)
                {
                    serviceEndpoint.Binding = CreateBinding(selectedEndpoint.Binding, selectedEndpoint.BindingConfiguration, serviceModelGroup);
                }

                if (serviceEndpoint.Address == null)
                {
                    serviceEndpoint.Address = new EndpointAddress(selectedEndpoint.Address, GetIdentity(selectedEndpoint.Identity), selectedEndpoint.Headers.Headers);
                }

                if (serviceEndpoint.Behaviors.Count == 0 && !string.IsNullOrEmpty(selectedEndpoint.BehaviorConfiguration))
                {
                    AddBehaviors(selectedEndpoint.BehaviorConfiguration, serviceEndpoint, serviceModelGroup);
                }

                serviceEndpoint.Name = selectedEndpoint.Contract;
            }

            return serviceEndpoint;
        }

        /// <remarks>This code is empty on purpose to prevent the default configuration from behing attempted. The configuration of the 
        /// endpoint is completed in the CreateDescription method because the config section is already loaded up there.</remarks>
        protected override void ApplyConfiguration(string configurationName)
        {
        }

        /// <summary>
        /// Configures the binding for the selected endpoint
        /// </summary>
        private Binding CreateBinding(string bindingName, string bindingConfiguration, ServiceModelSectionGroup serviceModelGroup)
        {
            BindingCollectionElement bindingElementCollection = serviceModelGroup.Bindings[bindingName];
            Binding binding = null;

            if (bindingElementCollection.ConfiguredBindings.Count > 0)
            {
                IBindingConfigurationElement be = bindingElementCollection.ConfiguredBindings.FirstOrDefault(x => x.Name == bindingConfiguration);
                binding = GetBinding(be);

                if (binding != null)
                {
                    be.ApplyConfiguration(binding);
                }
            }

            return binding;
        }

        /// <summary>
        /// Helper method to create the right binding depending on the configuration element
        /// </summary>
        /// <param name="configurationElement"></param>
        /// <returns></returns>
        private Binding GetBinding(IBindingConfigurationElement configurationElement)
        {
            if (configurationElement is CustomBindingElement)
            {
                return new CustomBinding();
            }
            else if (configurationElement is BasicHttpBindingElement)
            {
                return new BasicHttpBinding();
            }
            else if (configurationElement is NetMsmqBindingElement)
            {
                return new NetMsmqBinding();
            }
            else if (configurationElement is NetNamedPipeBindingElement)
            {
                return new NetNamedPipeBinding();
            }
            else if (configurationElement is NetPeerTcpBindingElement)
            {
                return new NetPeerTcpBinding();
            }
            else if (configurationElement is NetTcpBindingElement)
            {
                return new NetTcpBinding();
            }
            else if (configurationElement is WSDualHttpBindingElement)
            {
                return new WSDualHttpBinding();
            }
            else if (configurationElement is WSHttpBindingElement)
            {
                return new WSHttpBinding();
            }
            else if (configurationElement is WSFederationHttpBindingElement)
            {
                return new WSFederationHttpBinding();
            }

            return null;
        }

        /// <summary>
        /// Adds the configured behavior to the selected endpoint
        /// </summary>
        private void AddBehaviors(string behaviorConfiguration, ServiceEndpoint serviceEndpoint, ServiceModelSectionGroup selectionGroup)
        {
            EndpointBehaviorElement behaviorElement = selectionGroup.Behaviors.EndpointBehaviors[behaviorConfiguration];
            for (int i = 0; i <= behaviorElement.Count - 1; i++)
            {
                BehaviorExtensionElement behaviorExtension = behaviorElement[i];
                object extension = behaviorExtension.GetType().InvokeMember("CreateBehavior", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, behaviorExtension, null);
                if (extension != null)
                {
                    serviceEndpoint.Behaviors.Add((IEndpointBehavior)extension);
                }
            }
        }

        /// <summary>
        /// Gets the endpoint identity from the configuration file
        /// </summary>
        private EndpointIdentity GetIdentity(IdentityElement element)
        {
            EndpointIdentity identity = null;
            PropertyInformationCollection properties = element.ElementInformation.Properties;
            if (properties["userPrincipalName"].ValueOrigin != PropertyValueOrigin.Default)
            {
                return EndpointIdentity.CreateUpnIdentity(element.UserPrincipalName.Value);
            }
            if (properties["servicePrincipalName"].ValueOrigin != PropertyValueOrigin.Default)
            {
                return EndpointIdentity.CreateSpnIdentity(element.ServicePrincipalName.Value);
            }
            if (properties["dns"].ValueOrigin != PropertyValueOrigin.Default)
            {
                return EndpointIdentity.CreateDnsIdentity(element.Dns.Value);
            }
            if (properties["rsa"].ValueOrigin != PropertyValueOrigin.Default)
            {
                return EndpointIdentity.CreateRsaIdentity(element.Rsa.Value);
            }
            if (properties["certificate"].ValueOrigin != PropertyValueOrigin.Default)
            {
                X509Certificate2Collection supportingCertificates = new X509Certificate2Collection();
                supportingCertificates.Import(Convert.FromBase64String(element.Certificate.EncodedValue));
                if (supportingCertificates.Count == 0)
                {
                    throw new InvalidOperationException("UnableToLoadCertificateIdentity");
                }
                X509Certificate2 primaryCertificate = supportingCertificates[0];
                supportingCertificates.RemoveAt(0);
                return EndpointIdentity.CreateX509CertificateIdentity(primaryCertificate, supportingCertificates);
            }

            return identity;
        }

        private bool IsWildcardMatchNullOrEmpty(string endpointConfigurationName)
        {
            if (string.IsNullOrEmpty(endpointConfigurationName))
            {
                return true;
            }

            return string.Equals(endpointConfigurationName, "*", StringComparison.Ordinal);
        }
    }
}
